// app/projects/[projectId]/stats/page.tsx 'use client'; import { use, useState, useEffect } from 'react'; import { BarChart3, TrendingUp, HardDrive, Users, Eye, Download, Upload, Calendar, FileText, FolderOpen, Activity } from 'lucide-react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Badge } from '@/components/ui/badge'; import { Progress } from '@/components/ui/progress'; import { useToast } from '@/hooks/use-toast'; import { cn } from '@/lib/utils'; interface ProjectStats { storage: { used: number; limit: number; fileCount: number; folderCount: number; byCategory: { public: number; restricted: number; confidential: number; internal: number; }; }; activity: { views: number; downloads: number; uploads: number; shares: number; trend: number; // 증감률 }; users: { total: number; active: number; byRole: { admin: number; editor: number; viewer: number; }; }; recent: { type: string; user: string; action: string; timestamp: string; details: string; }[]; } export default function ProjectStatsPage({ params }: { params: Promise<{ projectId: string }> }) { // Next.js 15에서 params를 unwrap const resolvedParams = use(params); const projectId = resolvedParams.projectId; const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); const [dateRange, setDateRange] = useState('30d'); const { toast } = useToast(); useEffect(() => { fetchStats(); }, [projectId, dateRange]); const fetchStats = async () => { try { setLoading(true); const response = await fetch( `/api/projects/${projectId}/stats?range=${dateRange}` ); if (!response.ok) { if (response.status === 403) { throw new Error('통계를 볼 권한이 없습니다'); } throw new Error('통계 로드 실패'); } const data = await response.json(); setStats(data); } catch (error: any) { toast({ title: '오류', description: error.message || '통계를 불러올 수 없습니다.', variant: 'destructive', }); } finally { setLoading(false); } }; const formatBytes = (bytes: number) => { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; const formatNumber = (num: number) => { return new Intl.NumberFormat('ko-KR').format(num); }; if (loading) { return (
{[...Array(8)].map((_, i) => (
))}
); } if (!stats) { return (

통계를 불러올 수 없습니다

); } const storagePercentage = (stats.storage.used / stats.storage.limit) * 100; return (
{/* 헤더 */}

프로젝트 통계

프로젝트 사용 현황과 활동 내역을 확인합니다

7일 30일 90일
{/* 주요 지표 */}
스토리지 사용량
{formatBytes(stats.storage.used)}
{/* */} {/*

총 {formatBytes(stats.storage.limit)} 중 {storagePercentage.toFixed(1)}% 사용

*/}
파일 수
{formatNumber(stats.storage.fileCount)}

폴더 {formatNumber(stats.storage.folderCount)}개 포함

활성 사용자
{stats.users.active}

전체 {stats.users.total}명 중

총 다운로드
{formatNumber(stats.activity.downloads)}
{stats.activity.trend > 0 ? ( ) : ( )} 0 ? "text-green-500" : "text-red-500" )}> {Math.abs(stats.activity.trend)}%
{/* 상세 통계 */}
{/* 파일 카테고리 분포 */} 파일 카테고리 카테고리별 파일 분포
Public
{stats.storage.byCategory.public}
Restricted
{stats.storage.byCategory.restricted}
Confidential
{stats.storage.byCategory.confidential}
Internal
{stats.storage.byCategory.internal}
{/* 활동 요약 */} 활동 요약 기간별 활동 내역
조회수
{formatNumber(stats.activity.views)}
다운로드
{formatNumber(stats.activity.downloads)}
업로드
{formatNumber(stats.activity.uploads)}
공유
{formatNumber(stats.activity.shares)}
{/* 최근 활동 */} 최근 활동 프로젝트 내 최근 활동 내역 {/* 패딩이 스크롤에 포함되도록 CardContent p-0 + 내부 래퍼에 패딩 */}
    {stats.recent.map((activity, index) => (
  • {activity.user} {" "}님이{" "} {activity.details} {activity.action === "upload" && "을(를) 업로드했습니다"} {activity.action === "download" && "을(를) 다운로드했습니다"} {activity.action === "view" && "을(를) 조회했습니다"} {activity.action === "share" && "을(를) 공유했습니다"}

    {new Date(activity.timestamp).toLocaleString()}

  • ))}
); }